/**
 * Copyright (c) 2010 Basil Shikin, BugStat Project
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.bugstat.exporters;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * This class represents record storage. It contains a set of bug records for a given
 * time. It could be written into a file (see <code>saveRecords</code>) or read from a file (see <code>loadRecords</code>).
 *
 * @author Basil Shikin
 *
 */
public class RecordStorage
{
    private final int headerLength;
    
    private int recordLength;
    
    private byte[] buffer;
    private int    usedBytes;
    
    /**
     * Create new record storage. Empty storage with no header will be created. 
     * 
     * @param headerLength  Header length.
     */
    public RecordStorage(int headerLength )
    {
        this( headerLength, -1 );
    }
    
    /**
     * Create new record storage. Empty storage with no header will be created. 
     * 
     * @param headerLength  Header length.
     * @param recordLength  Record length. This field may be set later by <code>setRecordLength()</code> method.
     */
    public RecordStorage(int headerLength, int recordLength)
    {
        this.headerLength = headerLength;
        this.recordLength = recordLength;
        
        buffer    = new byte[ headerLength ];
        usedBytes = 0;
    }

    
    /**
     * Set new record length.
     * 
     * @param recordLength Record length to use. Must be positive.
     */
    public void setRecordLength(int recordLength)
    {
        if ( recordLength < 0 ) throw new IllegalArgumentException("Invalid record length specified: " + recordLength);
        
        this.recordLength = recordLength;
    }

    public synchronized void writeByte(int recordId, int offset, byte value ) throws IOException
    {
        final int o = getOffset( recordId )  + offset;
        
        if ( o >= buffer.length ) throw new IllegalArgumentException("Invalid offset specified: " + o); 
        
        buffer[ o ] = value;
    }
    
    public synchronized int readByte(int recordId, int offset ) throws IOException
    {
        final int o = getOffset( recordId )  + offset;
        
        if ( o >= buffer.length ) throw new IllegalArgumentException("Invalid offset specified: " + o); 
        
        return buffer[ o ];
    }
    
    public synchronized void writeShort(int recordId, int offset, short value ) throws IOException
    {
        final int o = getOffset(recordId )  + offset;
        
        if ( o + 1 >= buffer.length ) throw new IllegalArgumentException("Invalid offset specified: " + o);
        
        buffer[ o ]     = (byte)((value >>> 8) & 0xFF);
        buffer[ o + 1 ] = (byte)((value >>> 0) & 0xFF);
    }
    
    public synchronized short readShort(int recordId, int offset ) throws IOException
    {
        final int o = getOffset(recordId )  + offset;
        
        if ( o + 1 >= buffer.length ) throw new IllegalArgumentException("Invalid offset specified: " + o);
        
        return (short) ( ( ((byte)buffer[ o + 0 ] & 0xFF) << 8) + 
                         ( ((byte)buffer[ o + 1 ] & 0xFF) << 0) ); 
    }
    
    public synchronized void writeLong(int recordId, int offset, long value ) throws IOException
    {
        final int o = getOffset(recordId )  + offset;
        
        if ( o + 8 >= buffer.length ) throw new IllegalArgumentException("Invalid offset specified: " + o);
        
        buffer[ o + 0 ] = (byte)((value >>> 56) & 0xFF);
        buffer[ o + 1 ] = (byte)((value >>> 48) & 0xFF);
        buffer[ o + 2 ] = (byte)((value >>> 40) & 0xFF);
        buffer[ o + 3 ] = (byte)((value >>> 32) & 0xFF);
        buffer[ o + 4 ] = (byte)((value >>> 24) & 0xFF);
        buffer[ o + 5 ] = (byte)((value >>> 16) & 0xFF);
        buffer[ o + 6 ] = (byte)((value >>> 8) & 0xFF);
        buffer[ o + 7 ] = (byte)((value >>> 0) & 0xFF);
    }
    
    public synchronized long readLong(int recordId, int offset ) throws IOException
    {
        final int o = getOffset(recordId )  + offset;
        
        if ( o + 8 >= buffer.length ) throw new IllegalArgumentException("Invalid offset specified: " + o);
        
        return  (((long)buffer[ o + 0 ] & 0xFF) << 56) +
                (((long)buffer[ o + 1 ] & 0xFF) << 48) + 
                (((long)buffer[ o + 2 ] & 0xFF) << 40)  + 
                (((long)buffer[ o + 3 ] & 0xFF) << 32)  + 
                (((long)buffer[ o + 4 ] & 0xFF) << 24)  + 
                (((long)buffer[ o + 5 ] & 0xFF) << 16)  + 
                (((long)buffer[ o + 6 ] & 0xFF) << 8)  +
                (((long)buffer[ o + 7 ] & 0xFF) << 0);
    }
    
    public boolean containsRecord(int recordId)
    {
        if ( recordLength < 0 ) throw new IllegalStateException("Record length is not set");
        
        return getOffset( recordId ) < usedBytes;
    }
    
    public synchronized int appendRecord() throws IOException
    {
        if ( recordLength < 0 ) throw new IllegalStateException("Record length is not set");
        
        if ( usedBytes + recordLength >= buffer.length )
        {
            int newCapacity = (buffer.length * 3)/2 + 1;
            if (newCapacity < 100 ) newCapacity = 100; // Allow space for at least 100 records
            
            growBuffer( newCapacity + recordLength );
        }
        
        usedBytes += recordLength;
        
        return (usedBytes - headerLength)/recordLength;
    }
    
    public synchronized void writeHeader( final byte[] headerBuffer ) throws IOException
    {
        if ( headerBuffer == null ) throw new IllegalArgumentException("Invalid header buffer specified");
        if ( headerBuffer.length != headerLength) throw new IllegalArgumentException("Header buffer length is not equal to decleared length");
        
        // Write header at offset zero
        System.arraycopy( headerBuffer, 0, buffer, 0, headerBuffer.length );
        
        if ( usedBytes == 0 ) 
        {
            usedBytes = headerLength;
        }
    }
    
    public synchronized byte[] getHeader()
    {
        final byte[] header = new byte[headerLength];
        
        System.arraycopy( header, 0, buffer, 0, headerLength );
        
        return header;
    }
    
    public synchronized int size()
    {
        return (usedBytes - headerLength)/recordLength;
    }
    
    synchronized void saveRecords( final File file ) throws IOException
    {
        if ( file == null ) throw new IllegalArgumentException("Invalid file specified");
        
        if (!file.exists() ) file.createNewFile();
        
        FileOutputStream fos = null;
        try
        { 
            fos = new FileOutputStream( file );
            fos.write( buffer, 0, usedBytes );
            fos.flush();
        }
        finally
        {
            if ( fos != null ) fos.close();
        }
    }
    
    synchronized void loadRecords( final File file ) throws IOException
    {
        if ( file == null )  throw new IllegalArgumentException("Invalid file specified");
        if ( !file.exists() ) throw new IllegalArgumentException("Specified file does not exist (" + file.getAbsolutePath() + ")");
        
        FileInputStream fis = null;
        DataInputStream dis = null;
        try
        {
            if ( file.length() > Integer.MAX_VALUE ) throw new RuntimeException("File is too long" );
            
            int length = (int)file.length();
            if ( length > buffer.length )
            {
                // Allocate a slightly bigger buffer to make sure to fit next file
                int newCapacity = ( (length)* 3)/2 + 1;
                buffer = new byte[ newCapacity ];
            }
            
            fis = new FileInputStream( file );
            dis = new DataInputStream( fis );
            
            dis.readFully( buffer, 0, length );
            usedBytes = length;
        }
        finally
        {
            if ( dis != null ) dis.close();
            if ( fis != null ) fis.close();
        }
         
    }
    
    int getUsedBytes()
    {
        return usedBytes;
    }
    
    byte[] getBuffer()
    {
        return buffer;
    }
    
    private synchronized void growBuffer( int newLength )
    {
        if ( newLength < buffer.length ) throw new IllegalArgumentException("New length is shorter then old length");
        
        final byte[] newBuffer = new byte[ newLength ];
        
        System.arraycopy( buffer, 0, newBuffer, 0, buffer.length );
        
        buffer = newBuffer;
    }
    
    private int getOffset( int recordId )
    {
        if ( recordLength < 0 ) throw new IllegalStateException("Record length is not set");
        
        return headerLength + (recordId - 1) * recordLength;
    }
}
